Ana içeriğe geç

Polimorfizm

Polimorfizm, Java'da bir nesnenin farklı şekillerde davranabilme özelliğidir. Bu, farklı sınıfların aynı metodu farklı şekillerde uygulaması anlamına gelir. Java'da polimorfizm, yani çok biçimlilik, üç şekilde gerçekleştirilebilir: aşırı yükleme (overloading), aşırı yazma (overriding) ve arayüzler.

  1. Aşırı Yükleme (Overloading)

Aşırı yükleme, aynı sınıfta aynı isme sahip farklı parametrelerle birden fazla metodun tanımlanmasına izin verir. Bu, aynı metodun farklı şekillerde çağrılmasını sağlar. Aşırı yükleme, Java'da statik bağlama olarak da bilinen bağlama türünden yararlanır.

public class Calculator {
public int add(int x, int y) {
return x + y;
}

public int add(int x, int y, int z) {
return x + y + z;
}
}


  1. Aşırı Yazma (Overriding)

Aşırı yazma, bir alt sınıfta bir üst sınıfta tanımlanan metodu aynı isimle ve aynı parametrelerle yeniden tanımlama işlemidir. Aşırı yazma, Java'da dinamik bağlama olarak da bilinen bağlama türünden yararlanır.

public class Animal {
public void makeSound() {
System.out.println("Animal is making a sound");
}
}

public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog is barking");
}
}

  1. Arayüzler (Interfaces)

Arayüzler, bir sınıfın belirli bir işlevselliği uygulamasını zorunlu kılmak için kullanılır. Arayüzler, bir sınıfın birden fazla arayüzü uygulamasına olanak tanır ve çoklu kalıtımın bir alternatifi olarak kullanılabilir.

public interface Animal {
void makeSound();
}

public class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Dog is barking");
}
}

public class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("Cat is meowing");
}
}

Polimorfizm, çoklu kalıtımdan farklıdır çünkü Java'da çoklu kalıtım desteklenmez. Ancak, bir sınıf birden fazla arayüzü uygulayabilir ve bu, aynı işlevselliğin farklı arayüzler üzerinden erişilebilmesini sağlar. Bu şekilde, Java'da çoklu kalıtımın alternatifi olarak polimorfizm kullanılabilir.

Çoklu Kalıtımın Riskleri

Çoklu kalıtımın en önemli riski, çakışmaya (ambiguity) ve karışıklığa (confusion) neden olabilmesidir. Çoklu kalıtım, bir sınıfın birden fazla üst sınıfı (parent class) olan alt sınıfların (child class) oluşturulmasına olanak tanır. Ancak, bir alt sınıfın birden fazla üst sınıftan miras aldığı özelliklerde çakışma olabilir.

Örneğin, şöyle bir senaryoda çakışma yaşanabilir: Bir sınıfın bir üst sınıfı A ve bir başka üst sınıfı B var. Hem A hem de B sınıfları aynı isimde bir metodu veya özelliği tanımlıyor olsun. Bu durumda, alt sınıfın hangi üst sınıfın metodunu veya özelliğini kullanacağı belirsiz hale gelir.

Bir diğer risk de, çoklu kalıtımın karmaşıklığıdır. Birden fazla üst sınıfın özelliklerini bir alt sınıfa aktarmak, kodun okunabilirliğini, bakımını ve geliştirilmesini zorlaştırabilir.

Bu nedenlerden dolayı, Java gibi birçok modern programlama dili, çoklu kalıtımı desteklememekte veya sınırlamalar getirmektedir. Bunun yerine, arayüzler (interfaces) ve kalıtım (inheritance) gibi diğer mekanizmalar kullanılarak benzer işlevsellik sağlanmaktadır.

Geç Bağlama

Java'da geç bağlama, obje yönelimli programlama (OOP) prensiplerinden biridir ve dinamik bağlama (dynamic binding) olarak da bilinir. Geç bağlama, bir metodu veya bir özelliği çağırdığınızda, o metot veya özellik, o anki nesne tipine göre çalışacak şekilde otomatik olarak seçilir.

Java'da geç bağlama, çoğu zaman polimorfizm (polymorphism) ile birlikte kullanılır. Polimorfizm, aynı isme sahip farklı metotların veya özelliklerin farklı nesne tiplerinde farklı şekillerde davranmasına izin verir. Bu sayede, farklı sınıfların aynı arayüzü uygulamasına olanak tanır.

Geç bağlama, özellikle kalıtım (inheritance) ve arayüzler (interfaces) gibi OOP kavramlarının kullanıldığı yerlerde önemlidir. Örneğin, bir alt sınıfın bir üst sınıfta tanımlanmış bir metodu ezmesi (override) durumunda, geç bağlama sayesinde o metodun hangi sınıfta çağrılacağı otomatik olarak belirlenir. Bu, kodun daha esnek ve genişletilebilir olmasına olanak tanır.

Java'da geç bağlama, "virtual method invocation" olarak da adlandırılır. Bu, JVM'in (Java Virtual Machine) çalışma zamanında, doğru metodun seçilmesini sağlamak için bir tablo kullanmasıdır. Bu tablo, nesne tipine göre hangi metodun çalıştırılacağını belirler. Bu sayede, çalışma zamanında metotların hangi nesne türüne bağlı olarak davranacağı dinamik olarak belirlenir.

public class Animal {
public void makeSound() {
System.out.println("The animal makes a sound");
}
}

public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks");
}
}

public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("The cat meows");
}
}

public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.makeSound(); // Çıktı: The dog barks
animal2.makeSound(); // Çıktı: The cat meows
}
}

Yukarıdaki kodda, Animal adında bir üst sınıf (parent class) ve Dog ve Cat adında iki alt sınıf (child class) var. Her iki alt sınıf da makeSound() adında bir metodu ezmiş (override) ve kendi özel uygulamalarını sağlamıştır.

Main sınıfında, Animal türünden animal1 ve animal2 nesneleri oluşturulmuştur. animal1 nesnesi Dog türünden oluşturulmuştur, animal2 nesnesi ise Cat türünden oluşturulmuştur. makeSound() metodu her iki alt sınıfta farklı şekilde uygulandığı için, her iki nesnenin makeSound() metodu çağrıldığında farklı çıktılar üretilir.

Burada, animal1.makeSound() çağrıldığında Dog alt sınıfındaki makeSound() metodu çalışır ve "The dog barks" çıktısı üretilir. animal2.makeSound() çağrıldığında ise Cat alt sınıfındaki makeSound() metodu çalışır ve "The cat meows" çıktısı üretilir. Bu, Java'nın geç bağlama özelliğinin bir örneğidir, çünkü hangi alt sınıfın metodu çağrılacağı, çalışma zamanında nesne türüne göre belirlenir.

Soyut Sınıflar

Java'da soyut sınıflar, tam olarak uygulanmamış (incomplete) sınıflardır ve diğer sınıfların genel tasarımlarını ve davranışlarını tanımlamak için kullanılırlar. Soyut sınıflar, en az bir soyut metoda sahip olmalıdır ve bir sınıfın soyut olarak tanımlanması için abstract anahtar kelimesi kullanılır.

Soyut sınıflar, alt sınıflarında tamamlanması gereken metotların genel bir şablonunu tanımlarlar. Alt sınıflar, soyut sınıftan miras alır ve soyut metotları uygulayarak tamamlarlar. Bu, alt sınıfların özelliklerini ve davranışlarını belirler ve kalıtım hiyerarşisinde daha özelleştirilmiş (specialized) sınıflar oluşturulmasına izin verir.

Soyut sınıfların örnekleri oluşturulamaz, ancak bir referans olarak kullanılabilirler. Yani, soyut sınıf türünden bir değişken oluşturulabilir, ancak bu değişkenin değeri null veya bir alt sınıf örneği olmalıdır.

Örneğin, aşağıdaki kodda Animal adında bir soyut sınıf tanımlanmıştır ve içinde makeSound() adında bir soyut metot bulunmaktadır. Dog adında bir alt sınıf da tanımlanmıştır ve makeSound() metodu uygulanmıştır.

abstract class Animal {
public abstract void makeSound();
}

class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks");
}
}

public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
animal1.makeSound(); // Çıktı: The dog barks
}
}

Yukarıdaki örnekte, Animal soyut sınıfı makeSound() adında bir soyut metot içerir. Dog alt sınıfı, makeSound() metotunu uygular ve Main sınıfında bir Dog örneği oluşturularak makeSound() metodu çağrılır. Burada, Animal soyut sınıfı, Dog alt sınıfı tarafından uygulanan makeSound() metodu aracılığıyla çağrılabilir.

Arayüzler

Java'da arayüzler, sınıflardan farklı olarak sadece metot tanımları ve sabitler içeren bir türdür. Arayüzler, belirli bir davranışı garanti altına almak için kullanılır ve sınıfların arayüzleri uygulaması beklenir. Arayüzler, benzer işlevleri olan sınıfların genişletilebilirliğini artırır ve daha kolay değiştirilebilir ve genişletilebilir kod yazmayı sağlar.

Arayüzler, interface anahtar kelimesi kullanılarak tanımlanır ve içinde metot tanımları (sadece başlıkları) ve sabitler bulunur. Arayüzlerde herhangi bir metot gövdesi veya uygulaması yoktur. Bunun yerine, arayüzü uygulayan sınıflar, arayüzdeki tüm metotları uygulamak zorundadır. Arayüzdeki tüm metotlar varsayılan olarak public ve abstract olarak tanımlanır.

Bir sınıf birden fazla arayüzü uygulayabilir ve arayüzler arasında hiyerarşi oluşturabilir. Arayüzler arasındaki miras, extends anahtar kelimesi kullanılarak gerçekleştirilir. Bir sınıfın birden fazla arayüzü uygulaması durumunda, her arayüzdeki tüm metotları uygulamak zorundadır.

Aşağıdaki örnekte Animal adında bir arayüz tanımlanmıştır ve içinde eat() ve move() adında iki metot tanımlanmıştır. Bird adında bir sınıf, Animal arayüzünü uygular ve eat() ve move() metotlarını uygular.

interface Animal {
public void eat();
public void move();
}

class Bird implements Animal {
public void eat() {
System.out.println("Bird eats.");
}

public void move() {
System.out.println("Bird flies.");
}
}

public class Main {
public static void main(String[] args) {
Bird bird1 = new Bird();
bird1.eat(); // Çıktı: Bird eats.
bird1.move(); // Çıktı: Bird flies.
}
}

Yukarıdaki örnekte, Animal arayüzü içinde eat() ve move() adında iki metot tanımlanmıştır. Bird sınıfı, Animal arayüzünü uygular ve eat() ve move() metotlarını uygular. Main sınıfında, Bird sınıfından bir örnek oluşturulur ve eat() ve move() metotları çağrılır.

Yerel Sınıflar

Java'da yerel sınıflar, bir metot içinde tanımlanan ve sadece o metot içinde kullanılabilen sınıflardır. Yerel sınıflar, başka sınıfların içinde tanımlanabilen iç sınıfların bir alt kümesidir. Yerel sınıflar, bir metot içinde tanımlanmalarına rağmen, normal sınıflar gibi özelliklere sahip olabilirler. Yani, sınıf değişkenleri, özellikleri, kurucu metotları ve metotları tanımlayabilirler.

Yerel sınıflar, aynı zamanda metot içinde tanımlanmış olan değişkenlere erişebilirler. Ancak, erişebilecekleri değişkenlerin final olarak tanımlanmış olması gerekmektedir. Bu, yerel sınıfların, metot içinde tanımlanmış olan değişkenlerin değerlerinin değişmesini önler ve yerel sınıfın ömrü boyunca bu değişkenlere erişebilmesini sağlar.

Yerel sınıfların en yaygın kullanımı, olay dinleyicileri gibi, bir sınıfın belirli bir özelliğinin veya işlevinin yürütülmesinde kullanılan geçici bir yardımcı sınıfın tanımlanmasıdır.

Aşağıdaki örnekte, bir printMessage() metodu tanımlanmıştır ve bu metot içinde bir yerel sınıf olan MessagePrinter tanımlanmıştır. Bu sınıf, bir print() metodu içerir ve bu metot, printMessage() metodu içindeki message adlı değişkene erişebilir.

public class Main {
public void printMessage() {
final String message = "Hello, World!";

class MessagePrinter {
public void print() {
System.out.println(message);
}
}

MessagePrinter printer = new MessagePrinter();
printer.print();
}

public static void main(String[] args) {
Main main = new Main();
main.printMessage();
}
}

Yukarıdaki örnekte, printMessage() metodu içinde MessagePrinter adlı bir yerel sınıf tanımlanmıştır. Bu sınıf, print() adında bir metot içerir ve bu metot, printMessage() metodu içinde tanımlanan message adlı değişkene erişebilir. main() metodu içinde printMessage() metodu çağrılır ve bu da MessagePrinter sınıfının oluşturulmasına ve print() metodu çağrılmasına neden olur.